Skip to content

Replace broken --headless device flow with setup instructions#34

Merged
wesm merged 14 commits intomainfrom
headless-oauth
Feb 3, 2026
Merged

Replace broken --headless device flow with setup instructions#34
wesm merged 14 commits intomainfrom
headless-oauth

Conversation

@wesm
Copy link
Copy Markdown
Owner

@wesm wesm commented Feb 3, 2026

Fixes #31.

Problem

The --headless flag attempted to use Google's OAuth device flow, but this was fundamentally broken: Google's device flow does not support Gmail scopes - only OpenID Connect, Drive, and YouTube are allowed.

See: https://developers.google.com/identity/protocols/oauth2/limited-input-device#allowedscopes

Solution

Remove the broken device flow code and replace --headless with clear setup instructions:

$ msgvault add-account user@example.com --headless

=== Headless Server Setup ===

Google's OAuth device flow does not support Gmail scopes, so --headless
cannot directly authorize. Instead, authorize on a machine with a browser
and copy the token to your server.

Step 1: On a machine with a browser, run:

    msgvault add-account user@example.com

Step 2: Copy the token file to your headless server:

    ssh user@server mkdir -p '/home/user/.msgvault/tokens'
    scp '/home/user/.msgvault/tokens/user@example.com.json' user@server:'/home/user/.msgvault/tokens/user@example.com.json'

Step 3: On the headless server, register the account:

    msgvault add-account user@example.com

The token will be detected and the account registered. No browser needed.
All msgvault commands (sync, tui, etc.) will work normally.

The token file is portable and auto-refreshes, so this approach works reliably.

Changes

  • Remove deviceFlow() and pollForToken() (~130 lines of broken code)
  • Remove ScopesDeviceFlow constant
  • Add PrintHeadlessInstructions(email, tokensDir) with 3-step guide
  • Instructions use configured tokensDir (respects MSGVAULT_HOME / custom data_dir)
  • Proper shell quoting via shellQuote() for paths with spaces/special chars
  • parseClientSecrets rejects TV/device clients (missing redirect_uris) with clear error
  • add-account creates source record when token already exists (enables step 3)
  • Update --headless flag help text

Testing

  • Added TestShellQuote for shell escaping
  • Added TestSanitizeEmail for filename sanitization
  • Added TestParseClientSecrets covering valid desktop/web clients, TV/device rejection, and malformed JSON

🤖 Generated with Claude Code

wesm and others added 2 commits February 3, 2026 07:04
Fixes #31. The device code flow had multiple issues:

1. No HTTP status code checking - errors were silently ignored
2. No error field checking in device response - if Google returned an
   error, we'd get empty URL/code and ExpiresIn=0, causing immediate
   "authorization timed out"
3. Hardcoded Scopes instead of m.config.Scopes - device flow ignored
   the manager's configured scopes

Now provides clear error messages, including specific guidance when
the OAuth client isn't configured for device flow (requires "TVs and
Limited Input devices" type in Google Cloud Console).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend required field validation to prevent empty device_code (which
would cause polling failures) and expires_in <= 0 (which would cause
immediate timeout).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
wesm and others added 4 commits February 3, 2026 07:15
Google's "TVs and Limited Input devices" OAuth clients don't include
redirect_uris in their client secrets JSON, causing google.ConfigFromJSON
to fail. Now we fall back to manual parsing for these clients.

This allows users to use --headless with TV-type OAuth credentials
without needing to also maintain Desktop app credentials.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Validate that auth_uri and token_uri are present in fallback parsing
- Remove deprecated urn:ietf:wg:oauth:2.0:oob redirect URL (unused for
  device flow anyway)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Google's device flow only supports the full mail.google.com scope,
not granular gmail.readonly/gmail.modify scopes. This causes "invalid_scope"
errors when trying to use device flow with standard scopes.

- Add ScopesDeviceFlow constant with mail.google.com scope
- Use ScopesDeviceFlow in device flow instead of manager's configured scopes
- Update saveToken to accept scopes parameter
- Preserve original scopes when refreshing tokens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Google's OAuth device code flow does not support Gmail scopes - only
OpenID Connect, Drive, and YouTube are allowed. Rather than keeping
broken code, `--headless` now prints instructions for the workaround:
authorize on a machine with a browser and copy the token file.

Changes:
- Remove deviceFlow() and pollForToken() from oauth.go
- Remove ScopesDeviceFlow constant
- Add PrintHeadlessInstructions() to display setup steps
- Simplify Authorize() to browser flow only (remove headless param)
- Update addaccount.go to call instructions function for --headless

The token file is portable and auto-refreshes, so the copy-token
approach works reliably for headless servers.

Fixes #31

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wesm wesm changed the title Fix headless OAuth: proper error handling for device flow Replace --headless device flow with instructions Feb 3, 2026
wesm and others added 8 commits February 3, 2026 07:58
After copying the token file, users need to run `add-account` again
on the headless server to register the account in the database.

Changes:
- Add Step 3 to headless instructions
- Fix add-account to create source record when token already exists
  (needed for headless workflow where token was copied)
- Update --headless flag help text

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address review feedback: headless instructions now use the configured
tokens directory instead of hardcoding ~/.msgvault/tokens/. This
ensures correct paths for users with MSGVAULT_HOME or custom data_dir.

- Extract sanitizeEmail() function for reuse
- Pass tokensDir to PrintHeadlessInstructions()
- Add test for sanitizeEmail()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wrap paths in single quotes to handle spaces and shell-sensitive
characters in token directory paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add shellQuote() that handles embedded single quotes using the
standard POSIX technique: ' -> '\''

This ensures the scp command works even if the token path contains
single quotes (unlikely but possible).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TV/device clients don't work with browser auth flow (no redirect_uris).
Now parseClientSecrets detects this and returns a helpful error message
instead of failing later at runtime.

Added tests for parseClientSecrets covering valid desktop client,
TV/device client rejection, and malformed JSON.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tokens directory must exist before scp can copy the token file.
Add ssh mkdir -p command to step 2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend parseClientSecrets to detect missing redirect_uris in both
installed and web client types, not just installed.

Added tests for valid web client and web client without redirect_uris.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wesm wesm changed the title Replace --headless device flow with instructions Replace broken --headless device flow with setup instructions Feb 3, 2026
@wesm
Copy link
Copy Markdown
Owner Author

wesm commented Feb 3, 2026

This is working now and I have validated it locally

@wesm wesm merged commit 211e96b into main Feb 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add-account --headless not working?

1 participant